package cn.dansj.common.utils.time;

import java.text.DateFormat;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

abstract class AbstractFormatCache<F extends Format> {
    private static final class ArrayKey {

        private static int computeHashCode(final Object[] keys) {
            final int prime = 31;
            int result = 1;
            result = prime * result + Arrays.hashCode(keys);
            return result;
        }

        private final Object[] keys;
        private final int hashCode;

        ArrayKey(final Object... keys) {
            this.keys = keys;
            this.hashCode = computeHashCode(keys);
        }

        @Override
        public boolean equals(final Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final AbstractFormatCache.ArrayKey other = (AbstractFormatCache.ArrayKey) obj;
            return Arrays.deepEquals(keys, other.keys);
        }

        @Override
        public int hashCode() {
            return hashCode;
        }
    }

    private static final ConcurrentMap<AbstractFormatCache.ArrayKey, String> cDateTimeInstanceCache = new ConcurrentHashMap<>(7);

    static String getPatternForStyle(final Integer dateStyle, final Integer timeStyle, final Locale locale) {
        final Locale safeLocale = TimeUtils.toLocale(locale);
        final AbstractFormatCache.ArrayKey key = new AbstractFormatCache.ArrayKey(dateStyle, timeStyle, safeLocale);
        return cDateTimeInstanceCache.computeIfAbsent(key, k -> {
            try {
                final DateFormat formatter;
                if (dateStyle == null) {
                    formatter = DateFormat.getTimeInstance(timeStyle, safeLocale);
                } else if (timeStyle == null) {
                    formatter = DateFormat.getDateInstance(dateStyle, safeLocale);
                } else {
                    formatter = DateFormat.getDateTimeInstance(dateStyle, timeStyle, safeLocale);
                }
                return ((SimpleDateFormat) formatter).toPattern();
            } catch (final ClassCastException ex) {
                throw new IllegalArgumentException("No date time pattern for locale: " + safeLocale);
            }
        });
    }

    private final ConcurrentMap<AbstractFormatCache.ArrayKey, F> cInstanceCache = new ConcurrentHashMap<>(7);

    protected abstract F createInstance(String pattern, TimeZone timeZone, Locale locale);

    F getDateInstance(final int dateStyle, final TimeZone timeZone, final Locale locale) {
        return getDateTimeInstance(dateStyle, null, timeZone, locale);
    }

    F getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) {
        return getDateTimeInstance(Integer.valueOf(dateStyle), Integer.valueOf(timeStyle), timeZone, locale);
    }

    private F getDateTimeInstance(final Integer dateStyle, final Integer timeStyle, final TimeZone timeZone, Locale locale) {
        locale = TimeUtils.toLocale(locale);
        final String pattern = getPatternForStyle(dateStyle, timeStyle, locale);
        return getInstance(pattern, timeZone, locale);
    }

    public F getInstance() {
        return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, TimeZone.getDefault(), Locale.getDefault());
    }

    public F getInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
        Objects.requireNonNull(pattern, "pattern");
        final TimeZone actualTimeZone = TimeUtils.toTimeZone(timeZone);
        final Locale actualLocale = TimeUtils.toLocale(locale);
        final AbstractFormatCache.ArrayKey key = new AbstractFormatCache.ArrayKey(pattern, actualTimeZone, actualLocale);
        return cInstanceCache.computeIfAbsent(key, k -> createInstance(pattern, actualTimeZone, actualLocale));
    }

    F getTimeInstance(final int timeStyle, final TimeZone timeZone, final Locale locale) {
        return getDateTimeInstance(null, timeStyle, timeZone, locale);
    }
}
